
module Msf::Exploit::SQLi
  class Common
    include Msf::Module::UI

    #
    #   Creates an instance of an SQL Injection object, users should use the create_dbms method of Msf::Exploit::SQLi instead
    #
    #   @param datastore [DataStore]
    #   @param framework [Framework]
    #   @param user_output [Rex::Ui::Text::Output::Stdio] 
    #   @param opts [Hash] a dictionary containing the parameters needed
    #   @option opts [Integer] truncation_length :  [Optional] The number of characters returned, if the query result is truncated
    #   @option opts [String] concat_separator : [Optional] The separator to use when concatenating rows (default ',')
    #   @option opts [String] second_concat_separator : [Optional] The separator to use when concatenating columns (default ';')
    #   @option opts [Boolean] safe : don't use group_concat, safer for large tables if group_concat truncates the result, but more queries will be performed
    #   @option opts [String] null_replacement : a string that will replace NULL values
    #   @option opts [Boolean] hex_encode_strings : encode strings as hex numbers, no quotes in the payload
    #   @option opts [Object] an encoder name, or a hash specifying a custom encoder, see Encoders in DBMS-specific classes
    #   @param query_proc [Proc] a block that will receive the payload, and should send the request to the target,
    #   - if it's a regular SQL injection, it should return the part of the response that is the query result (one row)
    #   - if it's a boolean-based blind SQL injection, it should return `true`, `false`, or a value that evaluates to one of them
    #   `true` if the query returned a result, false otherwise
    #   - if it's a time-based blind SQL injection, the return value does not matter, the time the block takes to run is used to leak information.
    #
    def initialize(datastore, framework, user_output, opts = {}, &query_proc)
      raise ArgumentError, 'Missing the block that does the requests' unless block_given?
      raise ArgumentError, 'Positional arguments can\'t be nil' if [datastore, framework, user_output].any?(&:nil?)

      check_opts(opts)
      @query_proc = query_proc
      @safe = opts[:safe]
      @concat_separator = opts[:concat_separator]
      @second_concat_separator = opts[:second_concat_separator] || ';'
      @null_replacement = opts[:null_replacement] || ''
      @truncation_length = opts[:truncation_length] if opts[:truncation_length] && opts[:truncation_length].is_a?(Integer) && opts[:truncation_length] > 0
      @hex_encode_strings = opts[:hex_encode_strings]
      @encoder = opts[:encoder]
      @datastore = datastore
      @framework = framework
      @user_output = user_output
    end

    #
    #   Queries the block with the given SQL query, without necessarily returning a result (needed for
    #   example when uploading a file using a time-based SQL injection, as it's not necessary to
    #   run multiple queries for that purpose), not to be overridden, it is guaranteed that the query
    #   will run only once.
    #   @param query [String] The SQL query to execute
    #   @return [void]
    #
    def raw_run_sql(query)
      vprint_status "{SQLi} Executing (#{query})"
      if @hex_encode_strings
        query = hex_encode_strings(query)
        vprint_status "{SQLi} Encoded to (#{query})"
      end
      @query_proc.call(query)
    end

    #
    #   Queries the block with the given SQL query, and returns the result, this method is overridden in
    #   blind SQL injection classes, implementing the logic of leaking one bit at a time, and working 
    #   exactly the same as this method.
    #   @param query [String] The SQL query to execute
    #   @return [String] The query results
    #
    def run_sql(query)
      raw_run_sql(query)
    end

    attr_reader :datastore, :framework
    attr_accessor :concat_separator, :second_concat_separator, :null_replacement, :truncation_length, :safe

    private

    #
    #  This method checks that the required options are present, and have valid values
    #  @param opts [Hash] the options provided by the user
    #  @return [void]
    #
    def check_opts(opts)
      raise ArgumentError, 'null_replacement option cannot contain single quotes' if opts[:null_replacement] && opts[:null_replacement].include?("'")
      raise ArgumentError, 'truncation_length must be an integer' if opts[:truncation_length] && !opts[:truncation_length].is_a?(Integer)
    end
  end
end
